package vodoo.gui.components;

import java.awt.*;
import java.awt.event.*;
import java.text.*;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.text.*;

/*
**  Group multiple MaskTextFields and JLabels to act like
**  a single component.
**
**  1)  A MaskTextField is a customized JTextField.
**  2)	The focus manager will only set focus on the first
**		text field added to the group.
**  3   The up/down/right/left arrow keys can be used to
**		move within the group.
**  4)	The getText() method returns the text of all
**		components added to the group.
**  5)  The getData() method returns the text of the
**		text fields added to the group.
*/
public class TextGroup extends JPanel implements KeyListener
{
	public final static int TAB_NEXT = 1;
	public final static int TAB_PREVIOUS = -1;
	public final static Border DEFAULT_BORDER = UIManager.getBorder( "TextField.border" );
	public final static Color DEFAULT_BACKGROUND = UIManager.getColor( "TextField.background" );
	public final static Font DEFAULT_FONT = new Font("monospaced", Font.PLAIN, 12);
	public final static Color DEFAULT_MASK_COLOR = Color.gray;
	public final static boolean DEFAULT_DISPLAY_PARTIAL_MASK = true;

	private static Border labelBorder = new EmptyBorder(0, 5, 0, 5);

	private String delimiters;
	private Color maskColor = DEFAULT_MASK_COLOR;
	private boolean displayPartialMask = DEFAULT_DISPLAY_PARTIAL_MASK;
	private List<MaskTextField> maskTextFields;

	private StringBuffer text = new StringBuffer(16);

	/*
	**  Convenience constructors
	*/
	public TextGroup(String mask, String delimiters)
	{
		this(mask, delimiters, null, null);
	}

	public TextGroup(String mask, String delimiters, FieldValidator validator)
	{
		this(mask, delimiters, validator, null);
	}

	public TextGroup(String mask, String delimiters, FieldValidator[] validators)
	{
		this(mask, delimiters, null, validators);
	}

	/*
	**  Complete constructer
	*/
	public TextGroup(String mask, String delimiters, FieldValidator validator, FieldValidator[] validators)
	{
		this.delimiters = delimiters;
		setLayout( new FlowLayout(FlowLayout.CENTER, 0, 0 ) );
		setBorder( DEFAULT_BORDER );
		setBackground( DEFAULT_BACKGROUND );
		setFont( DEFAULT_FONT );
		maskTextFields = new ArrayList<MaskTextField>();

		//  Parse the mask into mask text fields and labels

		StringTokenizer st = new StringTokenizer(mask, delimiters, true);
		int tokenCount = 0;

		while ( st.hasMoreTokens() )
		{
			String token = st.nextToken();

			if (delimiters.indexOf( token ) == -1)
			{
				FieldValidator fv = getNextValidator( tokenCount, validator, validators );
				addTextField( token, fv );
				tokenCount++;
			}
			else
			{
				addLabel( token );
			}
		}
	}

	/*
	**  When a single FieldValidator is available it is used for all text fields.
	**  When multiple FieldValidators are available use validator corresponding to current token.
	*/
	private FieldValidator getNextValidator(int tokenCount, FieldValidator validator, FieldValidator[] validators)
	{
		if (validator != null)
			return validator;

		if (validators != null && tokenCount < validators.length)
			return validators[tokenCount];
		else
			return null;
	}

	/*
	**  Create a mask text field and add it to the panel
	*/
	public MaskTextField addTextField(String mask, FieldValidator validator)
	{
		MaskTextField textField = new MaskTextField(mask.length(), mask);
		//  uncomment the following line when using JDK1.3
//		textField.setFocusTraversable( false );
		textField.setFont( null );
		textField.setBorder( null );
		textField.setOpaque( false );
		textField.setForeground( null );
		textField.setParent( this );
		textField.setValidator( validator );
		textField.addKeyListener( this );

		//  First mask field created can accept focus from the Focus Manager

		if (maskTextFields.size() == 0)
		{
			textField.setFocusTraversable( true );
		}

		maskTextFields.add( textField );

		super.add( textField );
		return textField;
	}

	/*
	**  Create a label and add it to the panel
	*/
	public JLabel addLabel(String delimiter)
	{
		JLabel label = new JLabel( delimiter );
		super.add( label );

		label.setBorder( labelBorder );
		label.setFont( null );

		return label;
	}

	/*
	**
	*/
	public void setText(String delimitedData)
	{
		//  Parse out the data from the delimited string

		StringTokenizer st = new StringTokenizer(delimitedData, delimiters, true);
		int tokenCount = 0;

		while ( st.hasMoreTokens() )
		{
			String token = st.nextToken();

			if (delimiters.indexOf( token ) == -1)
			{
				if (tokenCount < maskTextFields.size())
				{
					setText( tokenCount, token );
				}

				tokenCount++;
			}
		}
	}

	public void setText(int position, String text)
	{
		if (position < maskTextFields.size())
		{
			((MaskTextField)maskTextFields.get( position )).setText( text );
		}
	}

	/*
	**  Return the text with delimiters
	*/
	public String getText()
	{
		return getComponentText( true );
	}

	public String getText(int position)
	{
		if (position < maskTextFields.size())
		{
			return ((MaskTextField)maskTextFields.get( position )).getText();
		}
		else
			return "";
	}

	/*
	**  Return the text without delimiters
	*/
	public String getData()
	{
		return getComponentText( false );
	}

	/*
	**  Methods to control the mask text color
	*/
	public void setMaskColor(Color color)
	{
		maskColor = color;
	}

	public Color getMaskColor()
	{
		return maskColor;
	}

	/*
	**  Control how the background mask is rendered
	**  true: as characters are entered a partial mask is displayed
	**  false: once the first character is entered no mask is displayed
	*/
	public void setDisplayPartialMask(boolean value)
	{
		displayPartialMask = value;
	}

	public boolean isDisplayPartialMask()
	{
		return displayPartialMask;
	}

	/*
	**  Append the text from the individual components
	**  to form the text for the group component.
	*/
	private String getComponentText(boolean delimiterRequired)
	{
		text.setLength( 0 );
		Component[] components = getComponents();

		for (int i = 0; i < components.length; i++)
		{
			if (components[i] instanceof JTextField)
			{
				JTextField textField = (JTextField)components[i];
				text.append( textField.getText() );
			}

			if (delimiterRequired && components[i] instanceof JLabel)
			{
				JLabel label = (JLabel)components[i];
				text.append( label.getText() );
			}
		}

		return text.toString();
	}

	/*
	**  Set focus on the next/previous text field
	*/
	public void tab(JTextField textField, int direction)
	{
		Component[] components = getComponents();

		//  find the text field that currently has focus

		int i = 0;

		while ( components[i] != textField )
			i++;

		//  find the next/previous text field on the panel

		do
		{
			i = (i + direction + components.length) % components.length;
		}
		while ( ! (components[i] instanceof JTextField) );

		//  set focus on the text field

		components[i].requestFocus();
	}

	//  Implement KeyListener
	//  Override the default behaviour of certain keys

	public void keyPressed(KeyEvent e)
	{
		JTextField textField = (JTextField)e.getSource();

		switch (e.getKeyCode())
		{
			//  tab to next text field

			case KeyEvent.VK_UP:
				tab( textField, TAB_NEXT );
				break;

			//  tab to previous text field

			case KeyEvent.VK_DOWN:
				tab( textField, TAB_PREVIOUS );
				break;

			//  move to next character or tab to next field

			case KeyEvent.VK_RIGHT:
				if (textField.getCaretPosition() == textField.getDocument().getLength())
				{
					tab( textField, TAB_NEXT );
					e.consume();
				}
				break;

			//  move to previous character or tab to previous field

			case KeyEvent.VK_LEFT:
				if (textField.getCaretPosition() == 0)
				{
					tab( textField, TAB_PREVIOUS );
					e.consume();
				}
				break;
		}
	}

	public void keyTyped(KeyEvent e) {}
	public void keyReleased(KeyEvent e) {}

	/*
	**  Test the class
	*/
	public static void main(String[] args)
	{
		JFrame frame = new JFrame();
		frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
		JPanel panel = new JPanel( new GridLayout(0,2) );
		frame.setContentPane( panel );

		//  add date with no validation
		final TextGroup date1 = new TextGroup("YYYY/MM/DD", "/");
		date1.setBackground( Color.yellow );
		date1.setForeground( Color.red );
		panel.add( new JLabel("Date - not validated:") );
		panel.add( date1 );

		JButton textButton = new JButton( "Validate Date" );
		textButton.addActionListener( new ActionListener()
		{
			public void actionPerformed(ActionEvent e)
			{
				System.out.println( date1.getText(0) ); // get text from first field
				System.out.println( date1.getData() );  // get text, excluding delimiters
				String text = date1.getText(); // get text, including delimeters
				System.out.print( text );
				try
				{
					SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd");
					df.setLenient( false );
					df.parse(text);
					System.out.println(" is valid");
				}
				catch(Exception e2)
				{
					System.out.println(" is invalid");
				}
			}
		});
		panel.add( new JLabel("") );
		panel.add( textButton );

		//  add date with numeric validation
		FieldValidator[] validators = new FieldValidator[3];
		validators[0] = new IntegerValidator(1900, 2010);
		validators[1] = new IntegerValidator(1, 12, 2);
		validators[2] = new IntegerValidator(1, 31, 2);
		TextGroup date2 = new TextGroup("YYYY/MM/DD", "/", validators);
		date2.setDisplayPartialMask( false );
		date2.setMaskColor( Color.red );
		panel.add( new JLabel("Date - validated:") );
		panel.add( date2 );

		//  add phone number validate for integer characters
		FieldValidator numeric = new IntegerValidator();
		TextGroup phone = new TextGroup("(###)___-____", "()-", numeric);
		panel.add( new JLabel("Phone Number:") );
		panel.add( phone );

		//  add IP address
		IntegerValidator ipv = new IntegerValidator(1, 255);
		TextGroup ip = new TextGroup("   .   .   .   ", ".", ipv);
		ip.setText("1.2.3");
		ip.setText(3, "4");
		panel.add( new JLabel("IP Address:") );
		panel.add( ip );

		//  add signed integer field
		IntegerValidator signed = new IntegerValidator(-100, 100);
		signed.setSignRequired(IntegerValidator.ALWAYS);
		TextGroup signedField = new TextGroup("+   ", "", signed);
		panel.add( new JLabel("Signed Integer:") );
		panel.add( signedField );

		//  add postal code as stand alone MaskTextField
		MaskTextField postalCode = new MaskTextField(6, "");
		postalCode.setValidator(numeric);
		panel.add( new JLabel( "Postal Code:" ) );
		panel.add( postalCode );

		//  add stand alone MaskTextField
		MaskTextField name = new MaskTextField(20, "First Name");
		name.setMaskColor( Color.blue );
		name.setDisplayPartialMask( false );
		panel.add( new JLabel("Masked First Name:") );
		panel.add( name );

		frame.pack();
		frame.setVisible(true);
	}
}

/*
**	Customized JTextField with the following properties:
**
**  1)  Text automatically selected when focus gained
**  2)  Control for the number of characters allowed
**  3)  Automatic tab when text field is full
**  4)  Optional mask displayed in text field before entering data
**  5)  Optional character validation can be added
**  6)  ToolTip will display validation error message
*/
class MaskTextField extends JTextField
	implements DocumentListener, FocusListener, KeyListener
{
	private TextGroup parent;
	private String mask;
	private Color maskColor = TextGroup.DEFAULT_MASK_COLOR;
	private boolean displayPartialMask = TextGroup.DEFAULT_DISPLAY_PARTIAL_MASK;
	private boolean isFocusTraversable = true;
	private FieldValidator validator;
	private StringBuffer textBuffer = new StringBuffer();


	public MaskTextField(int size, String mask)
	{
		super(size);
		setMask( mask );

		if (mask.trim().length() == 0)
			setMask( null );

		getDocument().addDocumentListener( this );
		addFocusListener( this );
		addKeyListener( this );
	}

	/*
	**  Parent is set when this MaskTextField is part of a group
	*/
	public void setParent(TextGroup parent)
	{
		this.parent = parent;
	}

	/*
	**	Text displayed in the background of the component
	*/
	public void setMask(String mask)
	{
		this.mask = mask;
	}

	public String getMask()
	{
		return mask;
	}

	/*
	**  Methods to control the mask text color
	*/
	public void setMaskColor(Color color)
	{
		maskColor = color;
	}

	public Color getMaskColor()
	{
		if (parent != null)
			return parent.getMaskColor();
		else
			return maskColor;
	}

	/*
	**  Control how the background mask is rendered
	*/
	public void setDisplayPartialMask(boolean value)
	{
		displayPartialMask = value;
	}

	public boolean isDisplayPartialMask()
	{
		if (parent != null)
			return parent.isDisplayPartialMask();
		else
			return displayPartialMask;
	}

	/*
	**  Add class to perform simple validation of typed characters
	*/
	public void setValidator(FieldValidator validator)
	{
		this.validator = validator;
	}

	/*
	**
	*/
	public boolean isFocusTraversable()
	{
		return isFocusTraversable;
	}

	public void setFocusTraversable(boolean value)
	{
		isFocusTraversable = value;
	}

	/*
	**  Override to add a mask as a background to the text field
	*/
	public void paintComponent(Graphics g)
	{
		super.paintComponent(g);

		if (mask == null)
			return;

		int length = getDocument().getLength();

		//  Paint mask character in remaining text field space

		if (isDisplayPartialMask() && length < mask.length())
		{
			g.setColor( getMaskColor() );
			FontMetrics fm = getFontMetrics( getFont() );
			int y = getInsets().top + fm.getAscent();
			int x = getInsets().left + fm.stringWidth( getText() );
			g.drawString( mask.substring( length ), x, y );
			return;
		}

		//  Paint mask characters only when text field is empty

		if (!isDisplayPartialMask() && length == 0)
		{
			Dimension d = getSize();
			g.setColor( getMaskColor() );
			FontMetrics fm = getFontMetrics( getFont() );
			int y = getInsets().top + fm.getAscent();
			int x = getInsets().left + (int)((d.width - fm.stringWidth( mask )) / 2);
			g.drawString( mask, x, y );
			return;
		}

	}

	/*
	**  Override so that the default document is always a SizeDocument
	*/
	protected Document createDefaultModel()
	{
		return new SizeDocument( getColumns() );
	}

	//  Implement DocumentListener
	//  Automatically tab when the text field becomes full

	public void insertUpdate(DocumentEvent e)
	{
		Document doc = (Document)e.getDocument();

		if (doc.getLength() == getColumns())
		{
			Object parent = getParent();

			if (parent instanceof TextGroup)
			{
				((TextGroup)parent).tab(this, TextGroup.TAB_NEXT);
			}
			else
				FocusManager.getCurrentManager().focusNextComponent( this );
		}
	}

	public void removeUpdate(DocumentEvent e) {}
	public void changedUpdate(DocumentEvent e) {}

	//  Implement FocusListener
	//  Select all the text when a text field gains focus

		public void focusGained(FocusEvent e)
		{
			JTextField textField = (JTextField)e.getSource();
			textField.selectAll();
			textField.setToolTipText( null );
		}

		public void focusLost(FocusEvent e) {}

	//  Implement KeyListener

	public void keyTyped(KeyEvent e)
	{
		//  The following keys are always valid

		switch ( (int)e.getKeyChar())
		{
			case KeyEvent.VK_BACK_SPACE:
			case KeyEvent.VK_ESCAPE:
			case KeyEvent.VK_TAB:
				return;
		}

		//  Invoke the validator to make sure the key is valid

		if (validator != null)
		{
			setToolTipText( null );
			String possibleText = getPossibleText( e );

			if (! validator.isValid( possibleText ) )
			{
//				Toolkit.getDefaultToolkit().beep();
				setToolTipText( validator.getMessage() );
				Component c = (Component)e.getSource();
				c.dispatchEvent( new KeyEvent (c, KeyEvent.KEY_PRESSED, 0, KeyEvent.CTRL_MASK, KeyEvent.VK_F1, KeyEvent.CHAR_UNDEFINED ) );
				e.consume();
			}
		}
	}

	public void keyPressed(KeyEvent e) {}
	public void keyReleased(KeyEvent e) {}

	/*
	**  Get the possible text by:
	**  1) getting the current text,
	**  2) delete any selected text,
	**  3) insert typed character
	*/
	private String getPossibleText(KeyEvent e)
	{
		JTextField textField = (JTextField)e.getSource();

		textBuffer.setLength(0);
		textBuffer.append( textField.getText() );

		int start =  textField.getSelectionStart();
		int end = textField.getSelectionEnd();
		textBuffer.delete(start, end);

		textBuffer.insert(start, e.getKeyChar());

		return textBuffer.toString();
	}

	/*
	**  Control the number of characters that can be entered
	*/
	class SizeDocument extends PlainDocument
	{
		private int maximumFieldLength = 0;

		public SizeDocument(int maximumFieldLength)
		{
			this.maximumFieldLength = maximumFieldLength;
		}

		public void insertString(int offset, String text, AttributeSet a)
			throws BadLocationException
		{
			if (text == null)
				return;

			//  Make sure text does not exceed its maximum length

			if ( getLength() + text.length() > maximumFieldLength )
			{
				Toolkit.getDefaultToolkit().beep();
			}
			else
			{
				super.insertString(offset, text, a);
			}
		}
	}
}




